<?php

namespace PhpYes\Com;

/**
 * URI       = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
 * hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty
 * authority = [ userinfo "@" ] host [ ":" port ]
 *
 * foo://example.com:8042/over/there?name=ferret#nose
 * scheme authority       path       query       fragment
 *
 * The scheme and path components are required, though the path may be empty (no characters).
 * When authority is present, the path must either be empty or begin with a slash ("/") character.
 * When authority is not present, the path cannot begin with two slash characters ("//").
 * These restrictions result in five different ABNF rules for a path (Section 3.3),
 * only one of which will match any given URI reference.
 *
 * path = path-abempty  ; begins with "/" or is empty
 *      / path-absolute ; begins with "/" but not "//"
 *      / path-noscheme ; begins with a non-colon segment
 *      / path-rootless ; begins with a segment
 *      / path-empty    ; zero characters
 *
 * path-abempty  = *( "/" segment )
 * path-absolute = "/" [ segment-nz *( "/" segment ) ]
 * path-noscheme = segment-nz-nc *( "/" segment )
 * path-rootless = segment-nz *( "/" segment )
 * path-empty    = 0<pchar>
 *
 * segment       = *pchar
 * segment-nz    = 1*pchar
 * segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) ; non-zero-length segment without any colon ":"
 * pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
 */
class Url
{

    /**
     * split query fragment
     * @param string $qf
     * @return string[] - [query(without leading ?), fragment(without leading #)]
     */
    public static function split_qf($qf)
    {
        $ret = ['', ''];
        $qf = isset($qf) ? trim($qf) : '';
        if ($qf === '') {
            return $ret;
        }
        $len = strlen($qf);
        if ($len == 1) {
            return $qf === '?' || $qf === '#' ? $ret : [$qf, ''];
        }

        if ($qf[0] === '?') {
            $f_idx = strpos($qf, '#', 1);
            if ($f_idx) {
                $ret[0] = $f_idx > 1 ? substr($qf, 1, $f_idx - 1) : '';
                $ret[1] = $len > $f_idx + 1 ? substr($qf, $f_idx + 1) : '';
            } else {
                $ret[0] = substr($qf, 1);
            }
        } elseif ($qf[0] === '#') {
            $ret[1] = substr($qf, 1);
        } else {
            $f_idx = strpos($qf, '#', 1);
            if ($f_idx) {
                $ret[0] = substr($qf, 0, $f_idx);
                $ret[1] = $len > $f_idx + 1 ? substr($qf, $f_idx + 1) : '';
            } else {
                $ret[0] = $qf;
            }
        }
        return $ret;
    }

    /**
     * @param string $pqf
     * @return string
     */
    public static function path_of_pqf($pqf)
    {
        $pqf = isset($pqf) ? trim($pqf) : '';
        if ($pqf === '') {
            return '';
        }
        $len = strlen($pqf);
        if ($len == 1) {
            return $pqf === '?' || $pqf === '#' ? '' : $pqf;
        }

        $idx = -1;
        for ($i = 0; $i < $len; $i++) {
            $c = $pqf[$i];
            if ($c === '?' || $c === '#') {
                $idx = $i;
                break;
            }
        }
        if ($idx < 0) {
            return $pqf;
        } else {
            return $idx > 0 ? substr($pqf, 0, $idx) : '';
        }
    }

    /**
     * split path query fragment
     * @param string $pqf
     * @return string[] - [path, query(without leading ?), fragment(without leading #)]
     */
    public static function split_pqf($pqf)
    {
        $ret = ['', '', ''];
        $pqf = isset($pqf) ? trim($pqf) : '';
        if ($pqf === '') {
            return $ret;
        }
        $len = strlen($pqf);
        if ($len == 1) {
            return $pqf === '?' || $pqf === '#' ? $ret : [$pqf, '', ''];
        }

        $q_idx = -1;
        $f_idx = -1;
        for ($i = 0; $i < $len; $i++) {
            $c = $pqf[$i];
            if ($c === '?') {
                if ($q_idx < 0) {
                    $q_idx = $i;
                }
            } elseif ($c === '#') {
                $f_idx = $i;
                break; // 没必要再继续查找了, 也保证了 q_idx >= 0 && f_idx >= 0 时 f_idx > q_idx
            }
        }

        if ($q_idx >= 0) { // pqf = ...?...
            $ret[0] = $q_idx > 0 ? substr($pqf, 0, $q_idx) : '';
            if ($f_idx > $q_idx) { // pqf = ...?...#...
                $ret[1] = $f_idx > $q_idx + 1 ? substr($pqf, $q_idx + 1, $f_idx - $q_idx - 1) : '';
                $ret[2] = $len > $f_idx + 1 ? substr($pqf, $f_idx + 1) : '';
            } else {
                $ret[1] = $len > $q_idx + 1 ? substr($pqf, $q_idx + 1) : '';
            }
        } elseif ($f_idx >= 0) { // pqf = ...#...
            $ret[0] = $f_idx > 0 ? substr($pqf, 0, $f_idx) : '';
            $ret[2] = $len > $f_idx + 1 ? substr($pqf, $f_idx + 1) : '';
        } else {
            $ret[0] = $pqf;
        }

        return $ret;
    }

    /**
     * make path query fragment
     * @param string|null $path
     * @param string|null $query - without leading ?
     * @param string|null $fragment - without leading #
     * @return string
     */
    public static function make_pqf($path = null, $query = null, $fragment = null)
    {
        $path = isset($path) ? trim($path) : '';
        $query = isset($query) ? trim($query) : '';
        $fragment = isset($fragment) ? trim($fragment) : '';

        $pqf = '';
        if ($path !== '') {
            $pqf .= $path;
        }
        if ($query !== '') {
            $pqf .= '?' . $query;
        }
        if ($fragment !== '') {
            $pqf .= '#' . $fragment;
        }
        return $pqf;
    }

    /**
     * 常用型 url 拼接，非常用型 url 就自己拼接一下
     * @param string|null $scheme
     * @param string|null $host
     * @param string|null $path
     * @param string|null $query - without leading ?
     * @param string|null $fragment - without leading #
     * @return string
     */
    public static function make_url($scheme = null, $host = null, $path = null, $query = null, $fragment = null)
    {
        $scheme = isset($scheme) ? trim($scheme) : '';
        $host = isset($host) ? trim($host) : '';
        $path = isset($path) ? trim($path) : '';
        $query = isset($query) ? trim($query) : '';
        $fragment = isset($fragment) ? trim($fragment) : '';

        $url = '';
        if ($scheme !== '') {
            if ($host !== '') {
                $url = $scheme . '://' . $host;
            } else {
                $url = $scheme . ':';
            }
        } elseif ($host !== '') {
            $url .= $host;
        }

        if ($path !== '') {
            $url .= $path;
        }
        if ($query !== '') {
            $url .= '?' . $query;
        }
        if ($fragment !== '') {
            $url .= '#' . $fragment;
        }
        return $url;
    }

    /**
     * normalize_path
     * 为了性能，只对路径进行一次字符扫描来解决战斗，没有用正则和查找替换等，所以性能还是很高的。
     *
     * normalize_path 不负责 规范化 base_path,
     * 在多数需要把相对路径计算成绝对路径的现实情况下，base_path 一般都是 规范化的，
     * 例如网页中的相对路径链接，这些相对路径都是相对于这个网页的路径的，而这个网页的路径一般是规范化的，
     * 另外，在现实场景中，一般获取到路径后就会进行规范化，以便于后面的计算，
     * 如果每个使用路径的函数都对路径进行一次规范化，就会导致路径用几次就要进行几次规范化，这显然就导致不停的重复规范化。
     *
     * base_path = /.., /../.. 等结尾时 normalize_path 不会修正, 导致计算出 /path, /../path 这样错误的结果，
     * 为了避免计算结果出错，请传递 规范化的 base_path,
     * 如果 base_path 可能不规范化时可以 base_path = normalize_path(base_path) 先将 base_path 规范化，
     * 或者在 base_path 可能不规范化时可以先判断一下 base_path 是否以 /.. 结尾，如果是，就规范化一下，否则就不用规范化
     *
     * @param string $path
     * @param string|null $base_path
     * @return string
     */
    public static function normalize_path($path, $base_path = null)
    {
        $path = isset($path) ? trim($path) : '';
        $base_path = isset($base_path) ? trim($base_path) : '';

        $absolute_path = '';
        if ($path === '') {
            if ($base_path === '') {
                return '/';
            } else {
                $absolute_path = $base_path;
            }
        } elseif ($path[0] === '/') {
            $absolute_path = $path;
        } elseif ($base_path === '') {
            $absolute_path = $path;
        } else {
            $idx = strrpos($base_path, '/');
            if ($idx === false) {
                $absolute_path = $base_path . '/' . $path;
            } else {
                $absolute_path = substr($base_path, 0, $idx + 1) . $path;
            }
        }
        $len = strlen($absolute_path);
        if ($len < 2) {
            return $absolute_path;
        }

        // 不用正则和查找替换等是为了效率，下面的逻辑通过一次扫描来解决战斗

        $arr = [];
        $abc = '';
        for ($idx = 0; $idx < $len; $idx++) {
            $c = $absolute_path[$idx];
            if ($c === '/') {
                if ($abc === '') {
                    $abc = '/';
                } elseif ($abc[-1] === '/') { // / 遇到 /, //+ => /, 其实就是 abc[0], abc[-1] 保证 /x 时逻辑走下面的分支
                    continue;
                } elseif ($abc == '/.') { // /. 遇到 /, /./ => /, /././ => /./ => /
                    $abc = '/';
                } else { // xxx 或 ..(只能开头，因为后面只有遇到 / 才arr.push) 或 /xxx 或 /.. 遇到 /,
                    array_push($arr, $abc);
                    $abc = '/';
                }
            } else {
                $abc .= $c;
            }
        }
        if ($abc == '/.') {
            array_push($arr, '/');
        } else {
            array_push($arr, $abc);
        }

        $normal_path = [];
        foreach ($arr as $abc) {
            if ($abc == '/..') {
                array_pop($normal_path);
            } else {
                array_push($normal_path, $abc);
            }
        }

        return count($normal_path) > 0 ? implode('', $normal_path) : '/';
    }

    /**
     * relative_path
     * relative_path 不负责 规范化 path 和 base_path,
     * 所以调用时请传递 规范化路径，可以检查路径是否包含 /. (path.include?('/.'))来粗略判断一下 路径 是否是 规范化路径，如果是，就规范化一下，否则就不用规范化，
     * 其实不先判断路径 是否是 规范化路径，直接 path = normalize_path(path) 也是可以的，因为 RbFrm::Com::Url.normalize_path 的性能是很高的，也许比 String.include? 高，
     * 因为 RbFrm::Com::Url.normalize_path 实现时为了性能，只对路径进行一次字符扫描来解决战斗，没有用正则和查找替换等，所以性能还是很高的。
     *
     * 另外，在现实场景中，一般获取到路径后就会进行规范化，以便于后面的计算，
     * 如果每个使用路径的函数都对路径进行一次规范化，就会导致路径用几次就要进行几次规范化，这显然就导致不停的重复规范化。
     *
     * @param string $path
     * @param string $base_path
     * @return string
     */
    public static function relative_path($path, $base_path)
    {
        $path = isset($path) ? trim($path) : '';
        $base_path = isset($base_path) ? trim($base_path) : '';

        if (!($path && $path[0] === '/')) { // path start with /, /xxx
            return $path;
        }
        if (!($base_path && $base_path[0] === '/')) { // base_path start with /, /xxx
            return $path;
        }

        $split = explode('/', $path);
        $split_len = count($split);
        $base_split = explode('/', $base_path);
        $base_len = count($base_split) - 1;
        $base_split = array_slice($base_split, 0, $base_len); // 干掉最后一段，获取到的就是目录了

        $idx = 0;
        for ($idx = 1; $idx < $split_len && $idx < $base_len && $split[$idx] === $base_split[$idx]; $idx++) {
            ;
        }

        $relative = [];
        if ($idx < $base_len) {
            // split[0..(idx-1)] 和 base_split[0..(idx-1)] 匹配了
            // base_path 部分匹配
            array_push($relative, ...array_fill(0, $base_len - $idx, '..'));
            if ($idx < $split_len) {
                // path 部分匹配
                //  1. path 是一个目录(即 path[-1] == '/', 即以 / 结尾), 这时 split[-1] == '',
                //  然而 base_path 为了获取目录干掉了最后一段，自然就 base_split[-1] != '', 除非 base_path == '/',
                //  但如果 base_path == '/', 那么肯定 idx == base_split.length, 逻辑不会走到这里来，因为 path = '/' 都会导致 idx == 1
                //  2. path 文件 和 base_path 的目录 不同名
                //  path = '/'; base_path = '/xxx/'
                //  path = '/xxx/'; base_path = '/xxx/yyy/'
                //  path = '/xxx'; base_path = '/yyy/'
                array_push($relative, ...array_slice($split, $idx));
            } else {
                // path 完全匹配
                // 说明 path 文件 和 base_path 的部分目录 同名了，在现实场景中，一般不会存在这种情况
                // 说明 path 是一个目录，所以在最后拼接一个 /, 确保目录以 / 结尾
                // path = '/xxx'; base_path = '/xxx/yyy/'
                array_push($relative, ''); // 最后 implode('/', $relative) 确保以 / 结尾
            }
        } else {
            // idx == base_split.length 且 split[0..(idx-1)] 和 base_split[0..(idx-1)](就是 base_split) 匹配了
            // base_path 完全匹配
            if ($idx < $split_len) {
                // split.length > base_split.length
                // path = '/'; base_path = '/'
                // path = '/'; base_path = '/xxx'
                // path = '/xxx/'; base_path = '/xxx/'
                // path = '/xxx'; base_path = '/'
                // path = '/yyy'; base_path = '/xxx'
                // path = '/xxx/zzz'; base_path = '/xxx/'
                array_push($relative, ...array_slice($split, $idx));

                // split[-1] == '' 说明 path 是一个目录(即 path[-1] == '/', 即以 / 结尾), 这时 relative[-1] == '',
                // 如果 relative[-1] == '' 且 relative.length == 1 说明 path 就是 base_path 的目录，所以最好 return './',
                // relative.join('/') 无法处理这种情况，所以这里需要特殊处理一下
                if (count($relative) == 1 && $relative[0] === '') {
                    return './';
                }
            } else {
                // split.length == base_split.length
                // 说明 path 文件 和 base_path 的目录 同名了，在现实场景中，一般不会存在这种情况
                // 说明 path 是一个目录，要计算成相对路径的话，只能是 './'
                // path = '/xxx'; base_path = '/xxx/'
                // path = '/xxx'; base_path = '/xxx/yyy'
                return './';
            }
        }

        return implode('/', $relative);
    }

    /**
     * 用法 :
     * if url[0] == '/'
     * elsif url[0] == '.'
     * elsif url[0] == '?'
     * elsif url[0] == '#'
     * elsif url.start_with?('https://') || url.start_with?('http://')
     * elsif !is_http_url(url) # !!! note : is_http_url 主要用在这里
     * else
     * end
     *
     * 判断比较粗燥，没有对 url 进行拆解，判断可能存在误差，误差主要出现在 相对路径 那里，
     * 这里没有做精确判断的原因主要是: 做精确判断需要拆解 url，然后对各个组件(scheme, user, host, port, path, query, fragment) 中的字符进行判断.
     *
     * URL 定义虽然规定了各个组件的字符范围，但 URL 用于具体逻辑中时，具体逻辑代码很难做到完全支持 URL 定义中允许的所有字符，
     * 这涉及到业务逻辑是否需要或者是否有必要支持 URL 定义中允许的不常用字符，工作量 以及 开发进度 等问题，花时间做不需要的事情就是在浪费时间.
     * 显然，URL 定义对各个组件的字符范围有规定，但具体业务逻辑也对各个组件的字符范围有要求，
     * 同一个 url , nginx 处理就正确，但你的业务逻辑代码处理就出 bug, 这是很正常的一个事情.
     * 可见，URL 定义的标准不是具体业务逻辑的标准，URL 定义的标准是一个界限，大家都遵守的话可以保证大家开发的软件在对接时没有问题，
     * 具体业务逻辑的要求可能是 URL 标准中的一个子集，不超越 URL 标准的界限.
     *
     * 再回到这里来，这里不拆解 url, 不精确判断各个组件的字符也就合理了，这都交给具体业务逻辑处进行.
     * 具体业务逻辑处可能只需要简单的判断几个有效字符进行了，例如 : path.match?(%r<^\w[-/\w]*\w\.html$>), 不满足要求的就当作是无效 url
     *
     * @param string $url
     * @return bool
     */
    public static function is_http_url($url)
    {
        $url = isset($url) ? trim($url) : '';
        if ($url === '') {
            return false;
        }

        if ($url[0] === '/') {
            // if (strlen($url) > 1 && $url[1] === '/') {
            // } else {
            // }
            return true;
        } elseif ($url[0] === '.') { // 相对路径
            return true;
        } elseif ($url[0] === '?') { // 相对路径，当前路径?query
            return true;
        } elseif ($url[0] === '#') { // 相对路径，当前路径#fragment
            return true;
        } elseif (str_starts_with($url, 'https://') || str_starts_with($url, 'http://')) {
            return true;
        } elseif (preg_match('<^\w+://>', $url)) { // not http url, ftp://..., irc://..., ...
            return false;
        } elseif (str_starts_with($url, 'mailto:')) {
            return false;
        } else {
            // 相对路径
            // 可能存在 www.baidu.com/path, conf.json.env/PATH 这样的有歧义的 url,
            // 是判定成相对路径 还是 判定成没有 scheme 的 url 可能需要一种策略 或者更仔细的分析 url 或者 分析 url 所处的语意环境
            return true;
        }
        return true;
    }

    // url, scheme, user, host, port, path, query, fragment

    /**
     * @var string
     */
    public $url;

    /**
     * @var string
     */
    public $scheme;

    /**
     * @var string
     */
    public $user;

    /**
     * @var string
     */
    public $host;

    /**
     * @var string
     */
    public $port;

    /**
     * @var string
     */
    public $path;

    /**
     * @var string
     */
    public $query;

    /**
     * @var string
     */
    public $fragment;


    /**
     * @param string $url
     */
    public function __construct($url)
    {
        $this->url = $url;
        $this->parse();
    }

    /**
     * @return string|null
     */
    public function normal_scheme()
    {
        return isset($this->scheme) ? strtolower($this->scheme) : null;
    }

    /**
     * @return $this
     */
    private function parse()
    {
        $url = isset($this->url) ? trim($this->url) : '';
        if ($url === '') {
            return $this;
        }
        $url_len = strlen($url);
        if ($url_len == 1) {
            if ($url === '?') {
                $this->query = '';
            } elseif ($url === '#') {
                $this->fragment = '';
            } else {
                $this->path = $url;
            }
            return $this;
        }
        // url.length >= 2

        // :, @, /, ?, #
        // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
        // authority = [ userinfo "@" ] host [ ":" port ]
        // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
        // userinfo = user:password
        // authority 默认是 host, 遇到 @ 时将前面的切成 user, 遇到 : 时将后面的切成 port
        // 即 @ 在 host 中标志着 user 结束，: 在 host 中时标志着 port 开始
        // 默认 path, 遇到 : 时如果 scheme 是空，就将前面的切成 scheme, 否则保持为 path

        // $scheme_colon_idx = -1; // scheme_end_colon_index
        // $user_at_idx = -1; // user_end_at_index
        // $port_colon_idx = -1; // port_start_colon_index
        // $path_idx = -1; // path_start_index
        // $query_idx = -1; // ? index
        // $fragment_idx = -1; // # index

        $c_0 = $url[0];

        if ($c_0 === '#') { // "#" fragment
            $this->fragment = substr($url, 1);
            return $this;
        } elseif ($c_0 === '?') { // "?" query [ "#" fragment ]
            $fragment_idx = strpos($url, '#', 1);
            if ($fragment_idx && $fragment_idx >= 1) { // ?...#...
                $this->query = $fragment_idx > 1 ? substr($url, 1, $fragment_idx - 1) : '';
                $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
            } else {
                $this->query = substr($url, 1);
            }
            return $this;
        } elseif ($c_0 === '/') {
            if ($url[1] === '/') { // "//" authority path-abempty [ "?" query ] [ "#" fragment ]
                if ($url_len < 3) {
                    return $this;
                }
                // url.length >= 3
                $this->parse_apqf($url, 2, ...$this->apqf_idx($url, 2));
            } else { // path-absolute [ "?" query ] [ "#" fragment ]
                $this->parse_pqf($url, 0, ...$this->pqf_idx($url, 1));
            }
            return $this;
        }

        // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
        $scheme_colon_idx = -1;
        if (('a' <= $c_0 && $c_0 <= 'z') || ('A' <= $c_0 && $c_0 <= 'Z')) {
            $scheme_colon_idx = strpos($url, ':', 1);
            while ($scheme_colon_idx && $scheme_colon_idx >= 1) {
                if ($scheme_colon_idx == 1) { // c:
                    $this->scheme = $c_0;
                    break;
                }
                // scheme_colon_idx > 1, c...:
                $idx = 0;
                for ($idx = 1; $idx < $scheme_colon_idx; $idx++) {
                    $c = $url[$idx];
                    if (('a' <= $c && $c <= 'z')
                        || ('A' <= $c && $c <= 'Z')
                        || ('0' <= $c && $c <= '9')
                        || $c === '+'
                        || $c === '-'
                        || $c === '.'
                    ) {
                        continue;
                    }
                    break;
                }
                if ($idx >= $scheme_colon_idx) {
                    $this->scheme = substr($url, 0, $scheme_colon_idx);
                }
                break;
            }
        }
        if (empty($this->scheme)) { // path-rootless [ "?" query ] [ "#" fragment ]
            $this->parse_pqf($url, 0, ...$this->pqf_idx($url, 1));
            return $this;
        }
        // 有 scheme, scheme_colon_idx 有意义
        $len = $url_len - $scheme_colon_idx - 1;
        if ($len < 1) { // scheme:
            return $this;
        }
        $c_0 = $url[$scheme_colon_idx + 1];
        if ($len == 1) { // scheme:c
            if ($c_0 === '?') {
                $this->query = '';
            } elseif ($c_0 === '#') {
                $this->fragment = '';
            } else {
                $this->path = $c_0;
            }
            return $this;
        }
        // len >= 2, scheme:cc...

        if ($c_0 === '#') { // scheme:"#" fragment
            $this->fragment = substr($url, $scheme_colon_idx + 2);
            return $this;
        } elseif ($c_0 === '?') { // scheme:"?" query [ "#" fragment ]
            $idx = $scheme_colon_idx + 2;
            $fragment_idx = strpos($url, '#', $idx);
            if ($fragment_idx && $fragment_idx >= $idx) { // ?...#...
                $this->query = $fragment_idx > $idx ? substr($url, $idx, $fragment_idx - $idx) : '';
                $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
            } else {
                $this->query = substr($url, $idx);
            }
            return $this;
        } elseif ($c_0 === '/') {
            if ($url[$scheme_colon_idx + 2] === '/') { // scheme:"//" authority path-abempty [ "?" query ] [ "#" fragment ]
                if ($len < 3) {
                    return $this;
                }
                // len >= 3
                $idx = $scheme_colon_idx + 3;
                $this->parse_apqf($url, $idx, ...$this->apqf_idx($url, $idx));
            } else { // scheme:path-absolute [ "?" query ] [ "#" fragment ]
                $this->parse_pqf($url, $scheme_colon_idx + 1, ...$this->pqf_idx($url, $scheme_colon_idx + 2));
            }
            return $this;
        } else { // scheme:path-rootless [ "?" query ] [ "#" fragment ]
            $this->parse_pqf($url, $scheme_colon_idx + 1, ...$this->pqf_idx($url, $scheme_colon_idx + 2));
            return $this;
        }
        return $this;
    }

    /**
     * @param string $url - url.length > 2, 要求必须有 //
     * @param int $start_idx - >= 2
     * @return int[] - [user_at_idx, port_colon_idx, path_idx, query_idx, fragment_idx]
     */
    private function apqf_idx($url, $start_idx)
    {
        $url_len = isset($url) ? strlen($url) : 0;
        if ($url_len < 3) { // url_len <= 2
            return [-1, -1, -1, -1, -1];
        }
        if ($start_idx < 2 || $start_idx >= $url_len) {
            return [-1, -1, -1, -1, -1];
        }

        // user_at_idx < port_colon_idx < path_idx < query_idx < fragment_idx

        $user_at_idx = -1;
        $port_colon_idx = -1;
        $path_idx = -1;
        $query_idx = -1;
        $fragment_idx = -1;
        for ($idx = $start_idx; $idx < $url_len; $idx++) {
            $c = $url[$idx];
            if ($c === '@') {
                if ($user_at_idx > 0) {
                    continue;
                }
                if ($path_idx > 0 && $idx > $path_idx) {
                    continue;
                }
                if ($query_idx > 0 && $idx > $query_idx) {
                    continue;
                }
                $user_at_idx = $idx;
                $port_colon_idx = -1; // 用来保证 port_colon_idx 在 user_at_idx 后面
            } elseif ($c === ':') {
                if ($port_colon_idx > 0) {
                    continue;
                }
                if ($path_idx > 0 && $idx > $path_idx) {
                    continue;
                }
                if ($query_idx > 0 && $idx > $query_idx) {
                    continue;
                }
                $port_colon_idx = $idx;
            } elseif ($c === '/') {
                if ($path_idx > 0) {
                    continue;
                }
                if ($query_idx > 0 && $idx > $query_idx) {
                    continue;
                }
                $path_idx = $idx;
            } elseif ($c === '?') {
                if ($query_idx < 0) {
                    $query_idx = $idx;
                }
            } elseif ($c === '#') {
                $fragment_idx = $idx;
                break; // 没必要再继续查找了, 也保证了 query_idx >= 0 && fragment_idx >= 0 时 fragment_idx > query_idx
            }
        }
        return [$user_at_idx, $port_colon_idx, $path_idx, $query_idx, $fragment_idx];
    }

    /**
     * authority path [ "?" query ] [ "#" fragment ] 类型
     * 使用时注意参数类型和值范围，本函数不会充分校验参数，
     * 因为本函数作为 parse 的部分逻辑，内部使用，而且 parse 其实已经给要作为参数传递的变量都设置好了合适的值
     *
     * !!! 要求 auth_idx <= user_at_idx < port_colon_idx < path_idx < query_idx < fragment_idx
     * !!! 要求 auth_idx, query_idx, fragment_idx 必须正确，这样才能校验其它的参数
     *
     * @param string $url - url.length > 2, 要求必须有 //
     * @param int $auth_idx - >= 2, authority start index in url, right after //, example: //auth, auth_idx is index of alpha a(is 2)
     * @param int $user_at_idx - @ index in authority, -1 if none found
     * @param int $port_colon_idx - port : index in authority, -1 if none found
     * @param int $path_idx
     * @param int $query_idx - first ? index in url, -1 if none found
     * @param int $fragment_idx - first # index in url, -1 if none found
     */
    private function parse_apqf($url, $auth_idx, $user_at_idx, $port_colon_idx, $path_idx, $query_idx, $fragment_idx)
    {
        $url_len = isset($url) ? strlen($url) : 0;
        if ($url_len < 3) { // url_len <= 2
            return;
        }
        if ($auth_idx < 2 || $auth_idx >= $url_len) {
            return;
        }

        // user, host, port, path, query, fragment
        $auth_end = $url_len;
        $path_end = $url_len;

        // query, fragment
        if ($query_idx > 0) {
            $auth_end = $query_idx;
            $path_end = $query_idx;

            if ($fragment_idx > $query_idx) {
                $this->query = $fragment_idx > $query_idx + 1 ? substr($url, $query_idx + 1, $fragment_idx - $query_idx - 1) : '';
                $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
            } else {
                $this->query = $url_len > $query_idx + 1 ? substr($url, $query_idx + 1) : '';
            }
        } elseif ($fragment_idx > 0) {
            $auth_end = $fragment_idx;
            $path_end = $fragment_idx;

            $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
        }

        // path - 必须校验 path_idx
        if ($auth_idx <= $path_idx && $path_idx < $path_end) {
            $auth_end = $path_idx;
            $this->path = $path_end >= $url_len ? substr($url, $path_idx) : substr($url, $path_idx, $path_end - $path_idx);
        }

        // user, host, port - 必须校验 user_at_idx, port_colon_idx
        if ($auth_idx <= $user_at_idx && $user_at_idx < $auth_end) {
            $this->user = $user_at_idx > $auth_idx ? substr($url, $auth_idx, $user_at_idx - $auth_idx) : '';

            if ($user_at_idx < $port_colon_idx && $port_colon_idx < $auth_end) {
                $this->host = $port_colon_idx > $user_at_idx + 1 ? substr($url, $user_at_idx + 1, $port_colon_idx - $user_at_idx - 1) : '';

                $idx = $port_colon_idx + 1;
                if ($auth_end > $idx && $url_len > $idx) {
                    $this->port = $auth_end >= $url_len ? substr($url, $idx) : substr($url, $idx, $auth_end - $idx);
                } else {
                    $this->port = '';
                }
            } else {
                $idx = $user_at_idx + 1;
                if ($auth_end > $idx && $url_len > $idx) {
                    $this->host = $auth_end >= $url_len ? substr($url, $idx) : substr($url, $idx, $auth_end - $idx);
                } else {
                    $this->host = '';
                }
            }
        } elseif ($auth_idx <= $port_colon_idx && $port_colon_idx < $auth_end) {
            $this->host = $port_colon_idx > $auth_idx ? substr($url, $auth_idx, $port_colon_idx - $auth_idx) : '';

            $idx = $port_colon_idx + 1;
            if ($auth_end > $idx && $url_len > $idx) {
                $this->port = $auth_end >= $url_len ? substr($url, $idx) : substr($url, $idx, $auth_end - $idx);
            } else {
                $this->port = '';
            }
        } elseif ($auth_end > $auth_idx && $url_len > $auth_idx) {
            $this->host = $auth_end >= $url_len ? substr($url, $auth_idx) : substr($url, $auth_idx, $auth_end - $auth_idx);
        } else {
            $this->host = '';
        }
    }

    /**
     * @param string $url
     * @param int $start_idx - >= 0
     * @return int[] - [query_idx, fragment_idx]
     */
    private function pqf_idx($url, $start_idx)
    {
        $url_len = isset($url) ? strlen($url) : 0;
        if ($url_len < 1) {
            return [-1, -1];
        }
        if ($start_idx < 0 || $start_idx >= $url_len) {
            return [-1, -1];
        }

        $query_idx = -1;
        $fragment_idx = -1;
        for ($idx = $start_idx; $idx < $url_len; $idx++) {
            $c = $url[$idx];
            if ($c === '?') {
                if ($query_idx < 0) {
                    $query_idx = $idx;
                }
            } elseif ($c === '#') {
                $fragment_idx = $idx;
                break; // 没必要再继续查找了, 也保证了 query_idx >= 0 && fragment_idx >= 0 时 fragment_idx > query_idx
            }
        }
        return [$query_idx, $fragment_idx];
    }

    /**
     * path [ "?" query ] [ "#" fragment ] 类型
     * 使用时注意参数类型和值范围，本函数不会充分校验参数，
     * 因为本函数作为 parse 的部分逻辑，内部使用，而且 parse 其实已经给要作为参数传递的变量都设置好了合适的值
     *
     * !!! 要求 path_idx <= query_idx < fragment_idx
     *
     * @param string $url
     * @param int $path_idx - >= 0
     * @param int $query_idx - first ? index in url, -1 if none found
     * @param int $fragment_idx - first # index in url, -1 if none found
     */
    private function parse_pqf($url, $path_idx, $query_idx, $fragment_idx)
    {
        $url_len = isset($url) ? strlen($url) : 0;
        if ($url_len < 1) {
            return;
        }
        if ($path_idx < 0 || $path_idx >= $url_len) {
            return;
        }

        if ($query_idx > 0) {
            $this->path = $query_idx > $path_idx ? substr($url, $path_idx, $query_idx - $path_idx) : '';
            if ($fragment_idx > $query_idx) {
                $this->query = $fragment_idx > $query_idx + 1 ? substr($url, $query_idx + 1, $fragment_idx - $query_idx - 1) : '';
                $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
            } else {
                $this->query = $url_len > $query_idx + 1 ? substr($url, $query_idx + 1) : '';
            }
        } elseif ($fragment_idx > 0) {
            $this->path = $fragment_idx > $path_idx ? substr($url, $path_idx, $fragment_idx - $path_idx) : '';
            $this->fragment = $url_len > $fragment_idx + 1 ? substr($url, $fragment_idx + 1) : '';
        } else {
            $this->path = $path_idx <= 0 ? $url : substr($url, $path_idx);
        }
    }
}